EP19. 파인 튜닝과 LoRA
LoRA는 사전학습된 LLM의 원본 가중치를 동결하고, 작은 저랭크 어댑터 행렬(B×A)만 학습하는 PEFT 기법으로, 풀 파인튜닝의 비용 폭증과 파멸적 망각 문제를 동시에 완화한다.
0. 사전 필수 용어 (선행지식)
본 EP를 이해하려면 다음 5개 용어가 친숙해야 합니다. 익숙하지 않다면 시리즈 초반(EP0~EP8 선수지식) 또는 Hugging Face Course Ch.1·3을 먼저 학습합니다.
- 신경망 가중치 (Weight Matrix) — Transformer 안에서 입력 토큰 임베딩을 변환하는 행렬 파라미터. 학습이란 이 행렬의 숫자를 미세 조정하는 과정. 본문 §3·§6에서 W·ΔW로 표기.
- 사전학습(Pre-training) vs 파인튜닝(Fine-tuning) — 사전학습은 대규모 일반 데이터로 처음부터 학습. 파인튜닝은 사전학습 완료 모델을 도메인·작업 데이터로 추가 학습. §1·§2의 출발점.
- 어텐션 (Attention) / KQVO — Transformer의 핵심 연산. 토큰 간 관계를 Query(Q)·Key(K)·Value(V)·Output(O) 4개 가중치 행렬로 계산. §3·§4 Step 3에서 어댑터 적용 위치로 다룸.
- FFN / MLP (Feed-Forward Network) — 어텐션 직후 비선형 변환 층. ReLU/GeLU 비선형 함수가 만드는 "사고의 점프"가 깊은 추론을 가능하게 함. gate·up·down 3개 행렬. §4 Step 3, §8 비교.
- 저랭크 행렬 (Low-Rank Matrix) / 행렬 분해 — d×d 큰 행렬을 d×r, r×d (r ≪ d) 두 작은 행렬의 곱으로 근사. LoRA의 수학적 기반. §3·§6에서 핵심 도식.
📚 참고: HuggingFace NLP Course — Transformer 구조·토크나이저·파인튜닝 기초.
1. 주제 정의
파인 튜닝(Fine-tuning) 은 사전학습된 LLM을 특정 도메인·작업 데이터에 맞춰 추가 학습시키는 과정입니다. LoRA(Low-Rank Adaptation) 는 원본 가중치를 동결하고, 작은 저랭크 어댑터 행렬만 학습하는 대표적인 PEFT(Parameter-Efficient Fine-Tuning) 기법입니다.
핵심 아이디어: 전체 가중치 W ∈ ℝ^(d×d)를 직접 수정하지 않고, 변화량 ΔW를 두 저랭크 행렬의 곱 ΔW = B(d×r) × A(r×d) 로 표현해 학습 파라미터 수를 d² → 2·d·r 로 줄인다 (r ≪ d). 학습 후 원하면 W' = W + B×A 로 병합하여 추론 오버헤드도 제거할 수 있습니다.
2. 풀려는 문제
파인튜닝이 직면하는 실무 난점은 크게 4가지입니다.
- 문제 1 — 학습 비용 폭증: 풀 파라미터 튜닝은 GPU 시간 + 데이터 정제 + 재평가 비용까지 누적되어, 미래 추론 비용 절감 효과를 상회하는 경우가 많음. "오늘의 1억 ≠ 3년 뒤 1억"의 화폐 시간가치 문제.
- 문제 2 — 파멸적 망각(Catastrophic Forgetting): 이미 균형 잡힌 사전학습 신경망을 추가 학습시킬 때, 기존에 잘 동작하던 일반 능력(예: 양자역학 학습 후 뉴턴역학 응답 품질 저하)이 무너질 수 있음.
- 문제 3 — 재평가 비용: 새 데이터셋 평가뿐 아니라, 튜닝이 기존 능력에 어떤 영향을 미쳤는지까지 전 영역 재평가가 필요. 풀튜닝마다 반복.
- 문제 4 — 튜닝의 가치 휘발: 비싸게 튜닝하는 동안 더 좋은 베이스 모델(예: 다음 세대 GPT/Claude/Gemini)이 출시되면 기존 튜닝 가치가 급락.
💡 실무 노하우: 풀튜닝은 "큰 모델·큰 데이터·전담 ML 인프라"가 모두 갖춰진 조직에서만 ROI가 나옵니다. 그 외에는 LoRA(또는 LoRA + RAG)로 시작하는 것이 자본 효율적입니다. AI 인재의 높은 몸값은 본질적으로 "한 번에 끝낼 수 있는 노하우" 가치 — 즉 시행착오 비용 절감의 가격입니다.
3. 핵심 개념·구조
LoRA의 동작은 다음 요소로 구성됩니다.
- 원본 가중치 W (freeze): 사전학습된 어텐션 KQVO·FFN gate/up/down 등의 행렬을 그대로 두고 grad 계산 X.
- 어댑터 행렬 A (r × d): 학습 대상. 정규분포(Gaussian)로 초기화.
- 어댑터 행렬 B (d × r): 학습 대상. 0으로 초기화 → 학습 전 ΔW = 0 → 원본 성능 보존.
- 랭크 r (= K): 어댑터의 두께. 표현력과 파라미터 수의 트레이드오프 조절자. 보통 4~64.
- target_modules: 어댑터를 삽입할 가중치 위치 집합. 기본은 KQVO + FFN 전체.
- 병합(merge): 학습 후
W' = W + B×A흡수. 추론 그래프에서 어댑터 사라짐.
┌──────────────────────────────────────┐
│ Original Attention Layer (frozen) │
│ │
│ ┌─── W (d × d) ──────┐ │
│ │ 100% frozen │ │
│ └────────────────────┘ │
│ + │
│ ┌─── B (d × r) ──┐ │
│ │ trainable │ │
│ └────────┬───────┘ │
│ × │
│ ┌─── A (r × d) ──┐ │
│ │ trainable │ │
│ └────────────────┘ │
│ │
│ ΔW = B × A (rank ≤ r) │
│ W_new = W + ΔW │
└──────────────────────────────────────┘
4. 구현 가이드 (Do It Yourself)
시작 전 (Before you begin)
이 섹션을 완료하면 Hugging Face PEFT 라이브러리로 LoRA 어댑터를 정의·학습·병합하는 전 과정을 직접 코드로 구현하고 검증할 수 있습니다.
선수 조건:
- Python 3.10+
- pip install transformers peft accelerate bitsandbytes datasets
- GPU (CUDA) 1장 이상 (7B 모델 기준 VRAM ≥ 16GB)
- Hugging Face Hub 토큰 환경 변수 HF_TOKEN 설정
소요 시간: 약 30분 (코드 작성 + 1 epoch 미니 학습).
Step 1 — 베이스 모델·토크나이저 로딩
목표: 사전학습 LLM과 토크나이저를 4-bit 양자화로 메모리 절감하여 로드합니다.
다음 코드를 train_lora.py 에 추가합니다.
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
import torch
bnb = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.bfloat16,
)
base_model = AutoModelForCausalLM.from_pretrained(
"meta-llama/Llama-3.1-8B-Instruct",
quantization_config=bnb,
device_map="auto",
)
tokenizer = AutoTokenizer.from_pretrained("meta-llama/Llama-3.1-8B-Instruct")
이 코드는 QLoRA 패턴 (4-bit 양자화 + LoRA) 의 첫 단계입니다.
⚠️ 주의:
HF_TOKEN을 코드에 하드코딩하지 마세요.export HF_TOKEN=...환경 변수 사용.💡 실무 노하우:
bnb_4bit_compute_dtype=torch.bfloat16은 A100/H100에서 최적. 구형 GPU(T4)는float16사용.📚 참고: bitsandbytes README · Hugging Face PEFT.
✅ 확인: base_model 의 파라미터가 4-bit dtype 으로 로드되고 GPU에 분산 배치되었는지 print(base_model.hf_device_map) 로 확인.
Step 2 — LoRA 설정 (LoraConfig)
목표: 어댑터 랭크·적용 위치·dropout 을 선언합니다.
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training
base_model = prepare_model_for_kbit_training(base_model)
lora_config = LoraConfig(
r=16,
lora_alpha=32,
target_modules=["q_proj", "k_proj", "v_proj", "o_proj",
"gate_proj", "up_proj", "down_proj"],
lora_dropout=0.05,
bias="none",
task_type="CAUSAL_LM",
)
model = get_peft_model(base_model, lora_config)
model.print_trainable_parameters()
r=16— 어댑터 두께(rank). 8~32가 일반 sweet spot.lora_alpha=32— 스케일링 계수. 보통2*r.target_modules— Attention KQVO + FFN gate/up/down 전체. 강의에서 설명한 표준 적용 범위.
💡 실무 노하우: 자원이 빠듯하면
["q_proj", "v_proj"]만 남기세요 (지식 위주 튜닝 가성비). 추론 능력 확장이 목표면 FFN(gate/up/down)도 포함.📚 참고: Hu et al. 2021, "LoRA: Low-Rank Adaptation of Large Language Models" — 원 논문.
✅ 확인: model.print_trainable_parameters() 출력에서 학습 파라미터 비율이 전체 대비 0.1~1% 수준 인지 확인.
Step 3 — 학습 루프 (SFTTrainer)
목표: TRL의 SFTTrainer 로 데이터셋을 학습합니다.
from trl import SFTTrainer, SFTConfig
from datasets import load_dataset
ds = load_dataset("tatsu-lab/alpaca", split="train[:1000]")
args = SFTConfig(
output_dir="./lora-llama3",
per_device_train_batch_size=4,
gradient_accumulation_steps=4,
learning_rate=2e-4,
num_train_epochs=1,
logging_steps=10,
save_steps=100,
)
trainer = SFTTrainer(model=model, args=args, train_dataset=ds, tokenizer=tokenizer)
trainer.train()
trainer.save_model("./lora-llama3-final")
⚠️ 주의:
learning_rate=2e-4는 LoRA 전용 권장값. 풀 파인튜닝(1e-5)보다 한 자리 큼.
✅ 확인: ./lora-llama3-final/adapter_model.safetensors 가 생성되고 베이스 모델 대비 수십~수백 MB로 작은지 확인.
Step 4 — 추론 (어댑터 로드)
목표: 어댑터를 베이스 모델에 동적으로 붙여 추론합니다.
from peft import PeftModel
base = AutoModelForCausalLM.from_pretrained("meta-llama/Llama-3.1-8B-Instruct",
quantization_config=bnb, device_map="auto")
model = PeftModel.from_pretrained(base, "./lora-llama3-final")
out = model.generate(**tokenizer("LoRA의 핵심 원리는?", return_tensors="pt").to("cuda"),
max_new_tokens=128)
print(tokenizer.decode(out[0], skip_special_tokens=True))
✅ 확인: 도메인 응답이 학습 전 베이스 모델 응답과 다르게(개선되어) 나오는지 확인.
Step 5 — 동작 확인 (병합 후 서빙)
목표: 어댑터를 베이스 모델에 흡수시켜 추론 오버헤드를 제거합니다.
merged = model.merge_and_unload()
merged.save_pretrained("./llama3-lora-merged")
tokenizer.save_pretrained("./llama3-lora-merged")
예상 출력: ./llama3-lora-merged/ 폴더에 어댑터가 흡수된 풀 가중치 모델이 저장되며, 이후엔 PeftModel 없이 AutoModelForCausalLM.from_pretrained("./llama3-lora-merged") 로 일반 모델처럼 로딩됩니다. 추론 속도는 베이스 모델과 동일.
⚠️ 주의: 병합 후엔 어댑터를 다시 분리할 수 없습니다. 후속 튜닝 가능성이 있으면 어댑터 파일 별도 보관.
5. 적용 사례 (공신력 오픈소스)
- Hugging Face PEFT (
https://github.com/huggingface/peft) — LoRA·QLoRA·AdaLoRA·IA³ 등 PEFT 기법의 사실상 표준 라이브러리.LoraConfigAPI 가 본 가이드의 출발점. - HuggingFace Transformers (
https://github.com/huggingface/transformers) — 베이스 모델 로딩·토크나이저·생성 파이프라인. - TRL (Transformer Reinforcement Learning) (
https://github.com/huggingface/trl) —SFTTrainer·DPOTrainer로 LoRA + SFT/DPO 워크플로 지원. - bitsandbytes (
https://github.com/bitsandbytes-foundation/bitsandbytes) — 4-bit/8-bit 양자화. QLoRA의 핵심 의존성. - vLLM (
https://github.com/vllm-project/vllm) — 병합 후 모델 또는 LoRA 어댑터를 그대로 서빙.--enable-lora플래그로 다중 어댑터 hot-swap 지원. - Ollama (
https://github.com/ollama/ollama) — 병합된 LoRA 모델을Modelfile의FROM지시문으로 로컬 배포. - LLaMA-Factory (
https://github.com/hiyouga/LLaMA-Factory) — LoRA/QLoRA GUI/CLI 파인튜닝 프레임워크. - Axolotl (
https://github.com/axolotl-ai-cloud/axolotl) — 설정 파일 기반 LoRA 파인튜닝 자동화. - Unsloth (
https://github.com/unslothai/unsloth) — LoRA 학습 속도 2배·VRAM 절반 최적화. - LangChain (
https://github.com/langchain-ai/langchain) — 병합 후 LoRA 모델을HuggingFacePipeline으로 체인에 결합.
📚 참고: 위 OSS는 모두 GitHub stars ≥ 5K (대부분 ≥ 20K) 이며 Apache 2.0 또는 MIT 라이선스.
💡 실무 노하우: 입문은 PEFT + TRL 조합으로 시작하고, 자동화 단계로 진입하면 Axolotl 의 YAML 설정으로 실험 재현성을 확보하는 패턴이 일반적입니다.
6. 핵심 원리
LoRA의 효율성은 단 한 줄의 행렬 수식에서 나옵니다:
W_new = W_orig + B × A (rank(B×A) ≤ r ≪ d)
- 원리 1 — 저랭크 가정: 사전학습 모델을 도메인 데이터에 적응시킬 때 필요한 ΔW는 본질적으로 저랭크라는 경험적 관찰. 따라서 d×d 풀랭크 갱신 대신 2dr 파라미터로 충분.
- 원리 2 — 0 초기화의 묘: B = 0 이면 학습 전 ΔW = 0 → 어댑터 삽입만으로는 원본 출력 변화 없음. 학습이 진행되며 점진적으로 B가 0에서 벗어나 안정적으로 학습 신호가 누적.
7. 변형·확장
- QLoRA: 베이스 모델을 4-bit 양자화 + LoRA. 24GB GPU 1장으로 65B 모델 튜닝 가능 (Dettmers et al. 2023).
- AdaLoRA: rank를 학습 도중 동적으로 재분배. 중요한 레이어에 더 큰 r 할당.
- DoRA (Weight-Decomposed LoRA): ΔW를 방향·크기로 분해해 풀튜닝에 더 근접한 품질.
- IA³: A·B 행렬 대신 가중치 벡터로 스케일만 학습. LoRA 보다 더 적은 파라미터.
- LoRA + Prompt/Prefix Tuning 결합: 어댑터 + 학습 가능한 임베딩 동시 사용.
8. 다른 도구·접근과의 비교 (3-way)
| 측면 | LoRA | 풀 파인튜닝 | RAG (검색증강) |
|---|---|---|---|
| 학습 파라미터 | 0.1~1% | 100% | 0% (인덱스 구축만) |
| GPU 비용 | 낮음 (단일 GPU 가능) | 매우 높음 (클러스터) | 거의 없음 |
| 파멸적 망각 | 거의 없음 (원본 freeze) | 위험 큼 | 해당 없음 |
| 도메인 톤·스타일 | 강함 (학습) | 강함 (학습) | 약함 (검색 결과 의존) |
| 최신 지식 주입 | 학습 데이터 시점 고정 | 학습 데이터 시점 고정 | 인덱스 갱신만으로 즉시 |
| 추론 속도 (병합 후) | 베이스와 동일 | 베이스와 동일 | 검색 단계 오버헤드 |
| 사실성 (Hallucination) | 학습 데이터 의존 | 학습 데이터 의존 | 검색 결과로 근거 제공 |
9. 한계·트레이드오프
- 저랭크 가정의 한계 — 작업이 베이스 모델과 크게 동떨어지면 r=8~32로 부족. r 증가하면 LoRA 이점 희석.
- r·alpha 하이퍼파라미터 민감도 —
lora_alpha / r비율이 학습률처럼 작용. 잘못 잡으면 underfit/overfit. - target_modules 선택 의존성 — 어텐션만 vs FFN 포함 결정이 결과 품질에 직접 영향. 자동 결정 도구 부재.
- 병합 후 비가역성 —
merge_and_unload이후 어댑터 회수 불가. 다중 도메인 hot-swap이 필요하면 병합 보류. - 양자화 + LoRA의 수치 손실 — QLoRA는 4-bit 양자화로 일부 정확도 손실. 정밀도 민감 작업에는 LoRA(non-quantized) 추천.
10. 최신 권장 패턴 (2026 기준)
- QLoRA 표준화: 24GB GPU 1장으로 7B~13B 튜닝, 80GB 1장으로 70B 튜닝이 새로운 표준 (2026-05 기준).
- LoRA Hub / 다중 어댑터: 베이스 모델 1개에 도메인별 어댑터 N개를 동적 로딩하여 멀티테넌트 서빙 (vLLM
--enable-lora). - DPO + LoRA 결합: SFT-LoRA 이후 DPO-LoRA로 선호도 학습. TRL
DPOTrainer가 표준 (2026-05 기준). - r 자동 탐색: AdaLoRA·LoRA-FA 등 r 자동 결정 기법이 연구 단계에서 실무 단계로 이동 중.
- MoE + LoRA: Mixtral 류 MoE 모델에 expert 별 LoRA 적용 (2026-05 기준, vLLM 지원 확장 중).
💡 실무 노하우: 2026년 시점 신규 프로젝트라면 QLoRA + DPO + vLLM 서빙 조합이 사실상 디폴트입니다. RAG와 결합해 "도메인 톤은 LoRA, 사실은 RAG" 분업이 권장.
11. 메타인지 자기평가
본인 프로젝트에 LoRA 도입 가능성 검증 절차:
Step 1 — 현재 상태 점검
nvidia-smi
python -c "import torch; print(torch.cuda.is_available(), torch.cuda.get_device_name(0))"
pip show peft transformers trl bitsandbytes
Step 2 — 적용 가능성 평가
- 조건 1: 도메인 데이터 1k~100k 샘플 확보 가능 (소량이면 LoRA, 대량이면 풀튜닝 검토)
- 조건 2: 작업이 베이스 모델 능력과 가까움 (멀면 r 키우거나 풀튜닝)
- 조건 3: 응답 톤·스타일·도메인 용어가 목표 (RAG로는 부족)
- 조건 4: 평가셋과 메트릭이 정의됨 (없으면 튜닝 ROI 측정 불가)
Step 3 — 점진 적용
- 미니 데이터셋(100건)으로 r=8, 1 epoch 학습 → 형식·톤이 학습되는지 sanity check
- r=16, FFN 포함, 전체 데이터로 1~3 epoch 확장
- eval set으로 베이스 모델 대비 +N% 개선 여부 검증
- 통과하면 DPO 단계 진입, 미달이면 데이터 정제 또는 r 조정 회귀
- 최종 모델은
merge_and_unload후 vLLM·Ollama로 서빙
검증일: 2026-05-27 (Hugging Face PEFT main, vLLM 0.x, TRL 최신). 라이브러리 API는 출시 후 변경될 수 있으므로 실 구현 시 각 OSS 의 최신 README 재확인 권장.
클릭하거나 Space를 눌러 뒤집기